package com.hup.utils.jni;

import com.hup.utils.io.batch.BatchUtil;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;

/**
 * 用于创建c/cpp,生成dll;
 * <pre>
 * dll制作过程:
 * 1.创建java类
 *  2.加入/修改native方法
 *  3a.调用 {@link #buildCFile()},程序自动更新[.h],
 *  3b.手工修改[c,cpp]:
 * 4.调用 {@link #buildDll()},生成dll文件
 * 关于冗余文件[h,o]:
 *  它们是每次更新代码后都不一样的中间过程文件,
 *  完成dll后,可以删除,也不用上传到vcs上;
 *  只要保留dll和java代码即可
 * native类调用过程:
 * 1.生成dll
 * 2.类中加入LoadLibrary,方式参考 {@link JniClzSample};
 *  无dll或dll未完成时,也可以LoadLibrary,只是不能调用该类
 * 3.java代码调用native方法;
 * </pre>
 *
 * @apiNote 仅限于创建简单dll,无[导入库,动态库,静态库]等情况!复杂dll仍需用专业软件生成
 * @author hugan
 * @date 2020/10/9
 */
@Log4j2
public class DllBuilder {

    /**
     * <pre>
     * 含native方法的java类;
     * .根据此类创建c/cpp文件
     * </pre>
     */
    private final Class<?> clz;
    /**
     * c语言还是c++
     */
    private final boolean isCpp;

    /**
     * 调用者所在的module路径
     */
    private final String moduleDir;
    /**
     * clz便后的class文件路径
     */
    private final String classFilePath;
    /**
     * {@link #customCFileDir}
     */
    private final String defaultCFileDir;
    /**
     * 生成的c文件[c,cpp,h]的目录,需要末尾带斜杠!
     * 省缺时,程序会将c文件生成在 [src/main/cpp/com.xx.xx.clz]下(即把{@link #clz}的父目录[java]换成[cpp])
     */
    @Setter
    private String customCFileDir;

    /**
     * 如[E:/jdk1.8.0_201/],需要末尾带斜杠!
     * 省缺时,会通过二次处理System.getProperty("java.home")后得到;
     * 不同环境下可能有差异,所以提供setter
     */
    @Setter
    private String customJdkHome;

    /**
     * dll的生成路径;
     * 省缺时在resource/dll下
     */
    @Setter
    private String customDllDir;

    /**
     * gcc文件路径
     */
    @Setter
    private String gccPath = "E:/MinGw/bin/gcc";

    public DllBuilder(Class<?> clz, boolean isCpp) {
        if (clz == null) throw new IllegalArgumentException("clz不能为空");
        this.clz = clz;
        this.isCpp = isCpp;

        String clzPath = clz.getName().replace(".", File.separator) + ".class";
        log.info("clz={}", clzPath);
        moduleDir = System.getProperty("user.dir");
        log.info("moduleDir={}", moduleDir);

        //找clz编译后的class文件
        boolean isTestClz = false;//判断是否为测试包下的类
        File clzFile = new File(moduleDir + "/target/classes/" + clzPath);
        if (clzFile.exists()) {
            classFilePath = moduleDir + "/target/classes/";
        } else {
            isTestClz = true;
            clzFile = new File(moduleDir + "/target/test-classes/" + clzPath);
            if (clzFile.exists()) {
                classFilePath = moduleDir + "/target/classes/";
            } else {
                throw new IllegalArgumentException("没有build项目?\n找不到文件:" + clzFile);
            }
        }

        //初始化默认的c文件生成目录
        String packagePath = clzPath.substring(0, clzPath.lastIndexOf(File.separator) + 1);
        defaultCFileDir = moduleDir + File.separator + "src" + File.separator + "main" + File.separator
                + (isCpp ? "cpp" : "c") + File.separator
                + packagePath;
    }

    private static boolean exeCmd(String desc, String cmd) {
        ArrayList<String> resp = BatchUtil.builder().exec(cmd);
        if (resp.isEmpty()) {
            log.info("{}成功", desc);
            return true;
        } else {
            log.error("{}失败?[\n{}]", desc, resp);
            return false;
        }
    }

    private String getCFileDir() {
        String res = customCFileDir != null ? customCFileDir : defaultCFileDir;
        if (!res.endsWith(File.separator)) res += File.separator;
        return res;
    }

    private String getCFilePath() {
        return getCFileDir() + clz.getSimpleName() + (isCpp ? ".cpp" : ".c");
    }

    /**
     * 生成c文件,过程:
     * 1.执行[javah]命令,根据java类生成头文件[.h]
     * javah -jni -classpath [class的路径!!] -d [输出的目录] [类路径com.xx.xxClassName,不含扩展名]
     * 2.代码创建[c,cpp]文件,并写入文件头内容
     */
    public void buildCFile() {
        String cFileDir = getCFileDir();
        log.info("c文件生成路径={}", cFileDir);

        //生成h文件
        String cmd = "javah -jni -classpath "
                + classFilePath
                + " -d " + cFileDir
                + " " + clz.getName();
        log.info("cmd={}", cmd);
        if (!exeCmd("h文件生成", cmd)) return;

        //创建c文件
        File cFile = new File(getCFilePath());
        if (cFile.exists()) {
            log.info("已存在c文件,不修改原有文件={}", cFile.getAbsolutePath());
        } else {
            try (FileWriter fos = new FileWriter(cFile)) {
                fos.write("#include <jni.h>\n");
                fos.write("#include \"" + clz.getName().replace(".", "_") + ".h\"\n");
                fos.write("\n");
                log.info("c文件创建功能={}", cFile.getAbsolutePath());
            } catch (IOException e) {
                log.error("c文件创建文件失败={},\n{}", cFile.getAbsolutePath(), e.getMessage());
            }
        }
    }

    public void buildDll() {
        buildDll(null);
    }
    /**
     * 生成dll,过程:
     * 1.[gcc -c]:根据[c,cpp]生成[o]文件
     * E:\MinGw\bin\gcc -c -I"E:\jdk1.8.0_20\include" -I"E:\jdk1.8.0_20\include\win32" ./hup-commons/src/test/jni/HelloJni.c -o ./hup-commons/src/test/jni/HelloJni.o
     * -I:引入两个目录,为了引入jni.h和jni_md.h
     * 2.[gcc -o]:将[.o]转为链接库:[dll是windows的,so是安卓的],,
     * E:\MinGw\bin\gcc -Wl,--add-stdcall-alias -shared -o ./hup-commons/src/test/jni/HelloJni.dll ./hup-commons/src/test/jni/HelloJni.o -l gdi32
     * -l gdi32:在获取dpi时要引入的,否则编译失败!(意思是用到某些api时,需要加这个才能生成功能)
     *
     * @param includeDirs nullable,如[],可能达不到效果!复杂dll仍需用专业软件生成
     */
    public void buildDll(String[] includeDirs) {
        String jdkHome = customJdkHome != null ? customJdkHome : System.getProperty("java.home").replace("jre", "");
        if (!jdkHome.endsWith(File.separator)) jdkHome += File.separator;
        String oFilePath = getCFileDir() + clz.getSimpleName() + ".o";

        String cmdO = gccPath
                + " -c"
                //引入jdk的头文件
                + " -I\"" + jdkHome + "include\""
                + " -I\"" + jdkHome + "include" + File.separator + "win32\""
                + (includeDirs == null ? "" : getIncludeParam(includeDirs))
                //c/cpp文件的路径
                + " " + getCFilePath()
                //生成o文件的路径,与c文件路径相同
                + " -o " + oFilePath;
        log.info("cmdO={}", cmdO);
        if (!exeCmd("o文件生成", cmdO)) return;

        String dllDir = customDllDir != null ? customDllDir : (moduleDir + File.separator
                + "src" + File.separator + "main" + File.separator + "resources" + File.separator + "dll");
        if (!dllDir.endsWith(File.separator)) dllDir += File.separator;

        File dllPathFile = new File(dllDir);
        if (!dllPathFile.exists() && !dllPathFile.mkdirs()) throw new IllegalArgumentException("路径创建失败:" + dllDir);

        String dllFilePath = dllDir + clz.getSimpleName() + ".dll";
        log.info("dllFilePath={}", dllFilePath);
        String cmdDll = gccPath
                //?
                + " -Wl,--add-stdcall-alias -shared"
                + " -o"
                //dll路径
                + " " + dllFilePath
                //o文件路径
                + " " + oFilePath;
        if (isCpp) {
            //c++需要加这个
            cmdDll += " -lstdc++";
        }
        log.info("cmdDll={}", cmdDll);
        exeCmd("dll生成", cmdDll);
    }

    private String getIncludeParam(String[] includeDirs) {
        StringBuilder sb = new StringBuilder();
        for (String includeDir : includeDirs) {
            sb.append(" -I\"");
            sb.append(includeDir);
            sb.append(" \"");
        }
        return sb.toString();
    }


}
